2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18 #import "adiumPurpleRequest.h"
19 #import "ESPurpleRequestActionController.h"
20 #import "ESPurpleRequestWindowController.h"
21 #import "ESPurpleFileReceiveRequestController.h"
22 #import <Adium/NDRunLoopMessenger.h>
23 #import <Adium/AIContactAlertsControllerProtocol.h>
24 #import <AIUtilities/AIObjectAdditions.h>
25 #import <Adium/ESFileTransfer.h>
26 #import "AMPurpleRequestFieldsController.h"
28 #import <AdiumLibpurple/SLPurpleCocoaAdapter.h>
29 #import "AILibpurplePlugin.h"
30 #import <libintl/libintl.h>
33 * Purple requires us to return a handle from each of the request functions. This handle is passed back to use in
34 * adiumPurpleRequestClose() if the request window is no longer valid -- for example, a chat invitation window is open,
35 * and then the account disconnects. All window controllers created from adiumPurpleRequest.m should return non-autoreleased
36 * instances of themselves. They then release themselves when their window closes. Rather than calling
37 * [[self window] close], they should use purple_request_close_with_handle(self) to ensure proper bookkeeping purpleside.
41 #include <libpurple/jabber.h>
43 /* resolved id for Meanwhile */
50 * @brief Process button text, removing gtk+ accelerator underscores
52 * Textual underscores are indicated by "__"
54 NSString *processButtonText(NSString *inButtonText)
56 NSMutableString *processedText = [inButtonText mutableCopy];
58 #define UNDERSCORE_PLACEHOLDER @"&&&&&"
60 //Replace escaped underscores with our placeholder
61 [processedText replaceOccurrencesOfString:@"__"
62 withString:UNDERSCORE_PLACEHOLDER
63 options:NSLiteralSearch
64 range:NSMakeRange(0, [processedText length])];
65 //Remove solitary underscores
66 [processedText replaceOccurrencesOfString:@"_"
68 options:NSLiteralSearch
69 range:NSMakeRange(0, [processedText length])];
71 //Replace the placeholder with an underscore
72 [processedText replaceOccurrencesOfString:UNDERSCORE_PLACEHOLDER
74 options:NSLiteralSearch
75 range:NSMakeRange(0, [processedText length])];
77 return [processedText autorelease];
81 static void *adiumPurpleRequestInput(
82 const char *title, const char *primary,
83 const char *secondary, const char *defaultValue,
84 gboolean multiline, gboolean masked, gchar *hint,
85 const char *okText, GCallback okCb,
86 const char *cancelText, GCallback cancelCb,
87 PurpleAccount *account, const char *who, PurpleConversation *conv,
91 Multiline should be a paragraph-sized box; otherwise, a single line will suffice.
92 Masked means we want to use an NSSecureTextField sort of thing.
93 We may receive any combination of primary and secondary text (either, both, or neither).
95 id requestController = nil;
96 NSString *primaryString = (primary ? [NSString stringWithUTF8String:primary] : nil);
98 //Ignore purple trying to get an account's password; we'll feed it the password and reconnect if it gets here, somehow.
99 if ([primaryString rangeOfString:@"Enter password for "].location != NSNotFound) return [NSNull null];
101 NSMutableDictionary *infoDict;
102 NSString *okButtonText = processButtonText([NSString stringWithUTF8String:okText]);
103 NSString *cancelButtonText = processButtonText([NSString stringWithUTF8String:cancelText]);
105 infoDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:okButtonText,@"OK Text",
106 cancelButtonText,@"Cancel Text",
107 [NSValue valueWithPointer:okCb],@"OK Callback",
108 [NSValue valueWithPointer:cancelCb],@"Cancel Callback",
109 [NSValue valueWithPointer:userData],@"userData",nil];
112 if (primaryString) [infoDict setObject:primaryString forKey:@"Primary Text"];
113 if (title) [infoDict setObject:[NSString stringWithUTF8String:title] forKey:@"Title"];
114 if (defaultValue) [infoDict setObject:[NSString stringWithUTF8String:defaultValue] forKey:@"Default Value"];
115 if (secondary) [infoDict setObject:[NSString stringWithUTF8String:secondary] forKey:@"Secondary Text"];
117 [infoDict setObject:[NSNumber numberWithBool:multiline] forKey:@"Multiline"];
118 [infoDict setObject:[NSNumber numberWithBool:masked] forKey:@"Masked"];
120 AILog(@"adiumPurpleRequestInput: %@",infoDict);
122 requestController = [ESPurpleRequestWindowController showInputWindowWithDict:infoDict];
124 return (requestController ? requestController : [NSNull null]);
127 static void *adiumPurpleRequestChoice(const char *title, const char *primary,
128 const char *secondary, int defaultValue,
129 const char *okText, GCallback okCb,
130 const char *cancelText, GCallback cancelCb,
131 PurpleAccount *account, const char *who, PurpleConversation *conv,
132 void *userData, va_list choices)
134 AILog(@"adiumPurpleRequestChoice: %s\n%s\n%s ",
135 (title ? title : ""),
136 (primary ? primary : ""),
137 (secondary ? secondary : ""));
139 return [NSNull null];
142 //Purple requests the user take an action such as accept or deny a buddy's attempt to add us to her list
143 static void *adiumPurpleRequestAction(const char *title, const char *primary,
144 const char *secondary, int default_action,
145 PurpleAccount *account, const char *who, PurpleConversation *conv,
147 size_t actionCount, va_list actions)
149 NSString *titleString = (title ? [NSString stringWithUTF8String:title] : @"");
150 NSString *primaryString = (primary ? [NSString stringWithUTF8String:primary] : nil);
151 id requestController = nil;
155 if (primaryString && ([primaryString rangeOfString:@"has just asked to directly connect"].location != NSNotFound)) {
156 AIListContact *adiumContact = contactLookupFromBuddy(purple_find_buddy(account, who));
157 //Automatically accept Direct IM requests from contacts on our list
158 if (adiumContact && [adiumContact isIntentionallyNotAStranger]) {
161 //Get the callback for Connect, skipping over the title
162 va_arg(actions, char *);
163 ok_cb = va_arg(actions, GCallback);
165 ((PurpleRequestActionCb)ok_cb)(userData, default_action);
169 } else if (primaryString && ([primaryString rangeOfString:@"Accept chat invitation"].location != NSNotFound)) {
170 AIListContact *contact = contactLookupFromBuddy(purple_find_buddy(account, who));
171 [[[AIObject sharedAdiumInstance] contactAlertsController] generateEvent:CONTENT_GROUP_CHAT_INVITE
172 forListObject:contact
173 userInfo:[NSDictionary dictionary]
174 previouslyPerformedActionIDs:nil];
178 NSString *secondaryString = (secondary ? [NSString stringWithUTF8String:secondary] : nil);
179 NSMutableArray *buttonNamesArray = [NSMutableArray arrayWithCapacity:actionCount];
180 GCallback *callBacks = g_new0(GCallback, actionCount);
182 //Generate the actions names and callbacks into useable forms
183 for (i = 0; i < actionCount; i += 1) {
187 buttonName = va_arg(actions, char *);
188 [buttonNamesArray addObject:processButtonText([NSString stringWithUTF8String:buttonName])];
190 //Get the callback for that name
191 callBacks[i] = va_arg(actions, GCallback);
194 //Make default_action (or first if none specified) the last one
195 if (default_action < (int)actionCount-1) {
196 // If there's no default_action, assume the first one is, and move it to the end.
197 if (default_action == -1)
200 GCallback tempCallBack = callBacks[actionCount-1];
201 callBacks[actionCount-1] = callBacks[default_action];
202 callBacks[default_action] = tempCallBack;
204 [buttonNamesArray exchangeObjectAtIndex:default_action withObjectAtIndex:(actionCount-1)];
207 NSMutableDictionary *infoDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
208 buttonNamesArray,@"Button Names",
209 [NSValue valueWithPointer:callBacks],@"callBacks",
210 [NSValue valueWithPointer:userData],@"userData",
211 titleString,@"TitleString",nil];
213 // If we have both a primary and secondary string, use the primary as a header.
214 if (secondaryString) {
215 [infoDict setObject:primaryString forKey:@"MessageHeader"];
216 [infoDict setObject:secondaryString forKey:@"Message"];
218 [infoDict setObject:primaryString forKey:@"Message"];
221 AIAccount *adiumAccount = accountLookup(account);
223 [infoDict setObject:adiumAccount forKey:@"AIAccount"];
227 AIListContact *adiumContact = contactLookupFromBuddy(purple_find_buddy(account, who));
229 [infoDict setObject:adiumContact forKey:@"AIListContact"];
232 [infoDict setObject:[NSString stringWithUTF8String:who] forKey:@"who"];
235 requestController = [ESPurpleRequestActionController showActionWindowWithDict:infoDict];
238 return (requestController ? requestController : [NSNull null]);
241 static void *adiumPurpleRequestFields(const char *title, const char *primary,
242 const char *secondary, PurpleRequestFields *fields,
243 const char *okText, GCallback okCb,
244 const char *cancelText, GCallback cancelCb,
245 PurpleAccount *account, const char *who, PurpleConversation *conv,
248 id requestController = nil;
249 NSString *titleString = (title ? [[NSString stringWithUTF8String:title] lowercaseString] : nil);
252 [titleString rangeOfString:@"new jabber"].location != NSNotFound) {
253 /* Jabber registration request. Instead of displaying a request dialogue, we fill in the information automatically.
254 * And by that, I mean that we accept all the default empty values, since the username and password are preset for us. */
255 ((PurpleRequestFieldsCb)okCb)(userData, fields);
258 AILog(@"adiumPurpleRequestFields: %s\n%s\n%s ",
259 (title ? title : ""),
260 (primary ? primary : ""),
261 (secondary ? secondary : ""));
263 id self = (CBPurpleAccount*)account->ui_data; // for AILocalizedString
265 requestController = [[AMPurpleRequestFieldsController alloc] initWithTitle:title?[NSString stringWithUTF8String:title]:nil
266 primaryText:primary?[NSString stringWithUTF8String:primary]:nil
267 secondaryText:secondary?[NSString stringWithUTF8String:secondary]:nil
269 okText:okText?[NSString stringWithUTF8String:okText]:AILocalizedString(@"OK",nil)
271 cancelText:cancelText?[NSString stringWithUTF8String:cancelText]:AILocalizedString(@"Cancel",nil)
273 account:(CBPurpleAccount*)account->ui_data
274 who:who?[NSString stringWithUTF8String:who]:nil
278 GList *gl, *fl, *field_list;
279 PurpleRequestFieldGroup *group;
281 //Look through each group, processing each field
282 for (gl = purple_request_fields_get_groups(fields);
287 field_list = purple_request_field_group_get_fields(group);
289 for (fl = field_list; fl != NULL; fl = fl->next) {
293 PURPLE_REQUEST_FIELD_NONE,
294 PURPLE_REQUEST_FIELD_STRING,
295 PURPLE_REQUEST_FIELD_INTEGER,
296 PURPLE_REQUEST_FIELD_BOOLEAN,
297 PURPLE_REQUEST_FIELD_CHOICE,
298 PURPLE_REQUEST_FIELD_LIST,
299 PURPLE_REQUEST_FIELD_LABEL,
300 PURPLE_REQUEST_FIELD_ACCOUNT
301 } PurpleRequestFieldType;
305 PurpleRequestField *field;
306 PurpleRequestFieldType type;
308 field = (PurpleRequestField *)fl->data;
309 type = purple_request_field_get_type(field);
310 if (type == PURPLE_REQUEST_FIELD_STRING) {
311 if (strcasecmp("username", purple_request_field_get_label(field)) == 0) {
312 purple_request_field_string_set_value(field, purple_account_get_username(account));
313 } else if (strcasecmp("password", purple_request_field_get_label(field)) == 0) {
314 purple_request_field_string_set_value(field, purple_account_get_password(account));
321 // ((PurpleRequestFieldsCb)okCb)(userData, fields);
325 return (requestController ? requestController : [NSNull null]);
328 static void *adiumPurpleRequestFile(const char *title, const char *filename,
329 gboolean savedialog, GCallback ok_cb,
331 PurpleAccount *account, const char *who, PurpleConversation *conv,
334 id requestController = nil;
335 NSString *titleString = (title ? [NSString stringWithUTF8String:title] : nil);
337 if (titleString && [titleString isEqualToString:[NSString stringWithFormat:[NSString stringWithUTF8String:_("Export Sametime List for Account %s")],
338 purple_account_get_username(account)]]) {
339 NSSavePanel *savePanel = [NSSavePanel savePanel];
341 if ([savePanel runModalForDirectory:nil file:nil] == NSOKButton) {
342 ((PurpleRequestFileCb)ok_cb)(user_data, [[savePanel filename] UTF8String]);
345 } else if (titleString && [titleString isEqualToString:[NSString stringWithFormat:[NSString stringWithUTF8String:_("Import Sametime List for Account %s")],
346 purple_account_get_username(account)]]) {
347 NSOpenPanel *openPanel = [NSOpenPanel openPanel];
349 if ([openPanel runModalForDirectory:nil file:nil types:nil] == NSOKButton) {
350 ((PurpleRequestFileCb)ok_cb)(user_data, [[openPanel filename] UTF8String]);
354 PurpleXfer *xfer = (PurpleXfer *)user_data;
356 PurpleXferType xferType = purple_xfer_get_type(xfer);
358 if (xferType == PURPLE_XFER_RECEIVE) {
359 AILog(@"*** WARNING: File request: %s from %s on IP %s which wasn't handled by the file-recv-request signal",
360 xfer->filename,xfer->who,purple_xfer_get_remote_ip(xfer));
361 /* We should never get here. The file-recv-request signal is posted before we could. We handle that signal
362 * (in adiumPurpleSignals) and set a local filename when we do to prevent being prompted via the request_file() ui op.
365 } else if (xferType == PURPLE_XFER_SEND) {
367 * Um, yes, we've already set the local filename... which should be the same as the file name for the transfer itself...
368 * and we do, in fact, want to send. Call the OK callback immediately.
370 if (xfer->local_filename != NULL && xfer->filename != NULL) {
371 AILog(@"PURPLE_XFER_SEND: %x (%s)",xfer,xfer->local_filename);
372 ((PurpleRequestFileCb)ok_cb)(user_data, xfer->local_filename);
374 ((PurpleRequestFileCb)cancel_cb)(user_data, xfer->local_filename);
375 [[SLPurpleCocoaAdapter sharedInstance] displayFileSendError];
381 AILog(@"adiumPurpleRequestFile() returning %@",(requestController ? requestController : [NSNull null]));
382 return (requestController ? requestController : [NSNull null]);
386 * @brief Purple requests that we close a request window
388 * This is not sent after user interaction with the window. Instead, it is sent when the window is no longer valid;
389 * for example, a chat invite window after the relevant account disconnects. We should immediately close the window.
391 * @param type The request type
392 * @param uiHandle must be an id; it should either be NSNull or an object which can respond to close, such as NSWindowController.
394 static void adiumPurpleRequestClose(PurpleRequestType type, void *uiHandle)
396 id ourHandle = (id)uiHandle;
397 AILog(@"adiumPurpleRequestClose %@ (%i)",uiHandle,[ourHandle respondsToSelector:@selector(purpleRequestClose)]);
398 if ([ourHandle respondsToSelector:@selector(purpleRequestClose)]) {
399 [ourHandle purpleRequestClose];
401 } else if ([ourHandle respondsToSelector:@selector(closeWindow:)]) {
402 [ourHandle closeWindow:nil];
406 static void *adiumPurpleRequestFolder(const char *title, const char *dirname, GCallback ok_cb, GCallback cancel_cb,
407 PurpleAccount *account, const char *who, PurpleConversation *conv,
410 AILog(@"adiumPurpleRequestFolder");
415 static PurpleRequestUiOps adiumPurpleRequestOps = {
416 adiumPurpleRequestInput,
417 adiumPurpleRequestChoice,
418 adiumPurpleRequestAction,
419 adiumPurpleRequestFields,
420 adiumPurpleRequestFile,
421 adiumPurpleRequestClose,
422 adiumPurpleRequestFolder
425 PurpleRequestUiOps *adium_purple_request_get_ui_ops()
427 return &adiumPurpleRequestOps;
430 @implementation ESPurpleRequestAdapter
432 + (void)requestCloseWithHandle:(id)handle
434 AILog(@"purpleThreadRequestCloseWithHandle: %@",handle);
435 purple_request_close_with_handle(handle);